#!/usr/bin/env python
#
# SCF2: Simple Configuration Format, version 2
#
# SCF2 replaces the old simple_config.py module, which used a clunky (and
# hackish) line-oriented processing mechanism. SCF2 implements a three-pass
# interpreter for SCF files, with a separate scanner, parser, and semantic
# engine. This module implements the main class and semantic engine.
#
# Author:   Mike Murphy (mamurph@cs.clemson.edu)
# Revision: 14 July 2008
#
#   Copyright 2007-2008 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

import scanner, parser, sys
from messages import MSG_SEMANTIC_ERROR

CH_TUPLE_ITEM = 0
CH_TUPLE_TYPE = 1
CH_TUPLE_SOURCE = 2
CH_TUPLE_LINE = 3

PTI_PT = 0
PTI_LHS = 1
PTI_TYPE = 2
PTI_RHS = 3
PTI_FILENAME = 4
PTI_LINE = 5

def semantic_error(tup):
   filename = tup[PTI_FILENAME]
   line = tup[PTI_LINE]
   print >> sys.argv, MSG_SEMANTIC_ERROR % (filename, line)

def get_type(item):
   itemtype = '?'
   if isinstance(item, ConfigHandler):
      itemtype = 'record'
   elif isinstance(item, str):
      itemtype = 'string'
   elif isinstance(item, list):
      itemtype = 'list'
   elif isinstance(item, bool):
      itemtype = 'bool'
   return itemtype

def build_ch_dict(ch, parserlines):
   ok = True
   for tup in parserlines:
      if tup[PTI_PT] == 'assign':
         lhs = tup[PTI_LHS].lower()
         rhs = tup[PTI_RHS]
         ptype = tup[PTI_TYPE]
         fn = tup[PTI_FILENAME]
         line = tup[PTI_LINE]
         if ptype == 'record':
            subconf = ConfigHandler()
            chk = build_ch_dict(subconf, rhs)
            if not chk:
               ok = False
            ch.db[lhs] = (subconf, ptype, fn, line)
         else:
            ch.db[lhs] = (rhs, ptype, fn, line)
      elif tup[PTI_PT] == 'control' and tup[PTI_LHS] == 'unset':
         key = tup[PTI_RHS].lower()
         if key in ch.db:
            del ch.db[key]
      else:
         semantic_error(tup)
         ok = False
   return ok

class ConfigHandler(object):
   #
   # Constructor parameter:
   #      preload       A dictionary of data to be copied into the internal
   #                    dictionary of the new ConfigHandler object. Useful for
   #                    setting default values of configuration items.
   def __init__(self, preload=None):
      # Internal dictionary (scf2) maps keys to tuples, where the tuple is:
      # (value, type, filename, line) and the filename and line specify
      # the location where the value was last set ('' and -1 if preloaded)
      self.db = {}
      if preload != None:
         for key in preload.keys():
            item = preload[key]
            itemtype = get_type(item)
            self.db[str(key).lower()] = (item, itemtype, '', -1)


   # Returns the value of a parameter (key name) in the dictionary. Key
   # names are NOT case-sensitive. If a particular key is not in the
   # dictionary, return result_if_missing, which defaults to None.
   # If a result_if_missing parameter is not None, a type check will be
   # performed on the value in the dictionary, and result_if_missing
   # will be returned if the type of the value does not match the type
   # of result_is_missing. This behavior can be suppressed by setting
   # type_check to False.
   def getParam(self, parameter, result_if_missing = None, type_check = True):
      lp = parameter.lower()
      if lp in self.db:
         item = self.db[lp][CH_TUPLE_ITEM]
         if result_if_missing != None and type_check == True:
            if not isinstance(item, type(result_if_missing)):
               return result_if_missing
         return item
      else:
         return result_if_missing


   # Parses a configuration file and loads settings from the specified file.
   # Returns True upon success, False if an error occurs.
   def parseConfigFile(self, filename):
      chk, data = scanner.read_file(filename)
      if chk:
         my_scanner = scanner.CfgScanner(filename, data)
         chk, scanout = my_scanner.scan()
         my_parser = parser.CfgParser(chk, scanout)
         chk, parsed = my_parser.parse()
         if chk:
            chk = build_ch_dict(self, parsed)
      return chk


   # Returns a sorted list of all the key names in the database (but does not
   # include key names inside any records/subconfigurations)
   def getNames(self):
      keys = self.db.keys()
      keys.sort()
      return keys


   # Returns the type of a parameter, if present in the database, as a string.
   # Typenames are:
   #   record       Parameter refers to a record/sub-configuration
   #   list         Parameter refers to a list of values
   #   string       Parameter refers to a string value
   #   bool         Parameter refers to True or False
   #   n/a          Parameter is not in the database
   #   ?            There is a bug in this code module
   def getType(self, parameter):
      lp = parameter.lower()
      ptype = 'n/a'
      if lp in self.db:
         ptype = self.db[lp][CH_TUPLE_TYPE]
      return ptype


   # A recursively-callable method for creating a pretty-printing, indented
   # representation of the contents of the database. Clients should probably
   # use __repr__ instead (i.e. cast the ConfigHandler object to a string
   # with str())
   def toStr(self, indent):
      out = ''
      keys = self.db.keys()
      keys.sort()
      for key in keys:
         item = self.db[key][CH_TUPLE_ITEM]
         fn = self.db[key][CH_TUPLE_SOURCE]
         line = self.db[key][CH_TUPLE_LINE]
         fnline = '(' + fn + ': ' + str(line) + ')'
         if line < 0:
            fnline = '(preload)'
         out += ' ' * indent + key + ' = '
         if self.getType(key) == 'record':
            out += '{ ' + fnline + '\n'
            out += self.db[key][CH_TUPLE_ITEM].toStr(indent + 3)
            out += ' ' * indent + '}'
         else:
            out += str(self.db[key][CH_TUPLE_ITEM])
            out += ' ' + fnline
         out += '\n'
      return out


   def __repr__(self):
      return self.toStr(0)

   def __contains__(self, item):
      return item.lower() in self.db.keys()

   def __getitem__(self, key):
      if key.lower() in self.db.keys():
         return self.getParam(key)
      raise KeyError

   def __iter__(self):
      return self.db.__iter__()

   # NEW for scf2: retrieves information about the source location
   # (filename and line) where a key was last set
   def getSource(self, key):
      source = None
      lp = key.lower()
      if lp in self.db:
         source = (self.db.lp[CH_TUPLE_SOURCE], self.db.lp[CH_TUPLE_LINE])
      return source


if __name__ == '__main__':
   ch = ConfigHandler({'chpreload' : True})
   ch.parseConfigFile(sys.argv[1])
   print ch